// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2020-2020 All rights reserved.
* Description: Implement liblinux pal kernfs
* Author: Huawei OS Kernel Lab
* Create: Tue Nov 07 04:28:40 2020
*/

#include <linux/fs.h>
#include <linux/idr.h>
#include <linux/file.h>
#include <linux/stat.h>
#include <linux/mount.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <liblinux/kernfs.h>
#include <uapi/linux/mount.h>
#include <linux/init_syscalls.h>
#include <liblinux/sysfs.h>
#include <trace/hooks/liblinux.h>

#include "../../../fs/kernfs/kernfs-internal.h"
#include "../../../fs/sysfs/sysfs.h"

static struct vfsmount *sysfs_mnt;
DEFINE_IDR(sysfs_idr);
static int liblinux_fs_tag_min = INT_MAX;
static int liblinux_fs_tag_max = INT_MIN;
struct mutex g_liblinux_lock;

extern int kernfs_get_target_path(struct kernfs_node *parent, struct kernfs_node *target,
					char *path);
extern struct file *alloc_empty_file(int, const struct cred *);

struct file *alloc_file(const struct path *path, int flags,
			const struct file_operations *fop)
{
	struct file *file;

	file = alloc_empty_file(flags, current_cred());
	if (IS_ERR(file))
		return file;

	file->f_path = *path;
	file->f_inode = path->dentry->d_inode;
	file->f_mapping = path->dentry->d_inode->i_mapping;

	if ((file->f_mode & FMODE_READ) &&
	     likely(fop->read || fop->read_iter))
		file->f_mode |= FMODE_CAN_READ;
	if ((file->f_mode & FMODE_WRITE) &&
	     likely(fop->write || fop->write_iter))
		file->f_mode |= FMODE_CAN_WRITE;
	file->f_mode |= FMODE_OPENED;
	file->f_op = fop;
	if ((file->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ)
		i_readcount_inc(path->dentry->d_inode);
	return file;
}

static struct inode *__liblinux_kernfs_iget(struct kernfs_node *kn)
{
	struct kernfs_root *root = kernfs_root(kn);

	if (sysfs_mnt == NULL || root == NULL)
		return NULL;

	if (root->kn != sysfs_root_kn) /* only support sysfs now */
		return NULL;

	return kernfs_get_inode(sysfs_mnt->mnt_sb, kn);
}

static int __liblinux_kernfs_open(struct dentry *dentry, int mode,
				  struct file **file)
{
	struct path path = {
		.mnt = NULL,
		.dentry = dentry,
	};
	struct file *f = NULL;
	struct inode *inode = dentry->d_inode;
	int ret = -EINVAL;

	f = alloc_file(&path, mode, fops_get(inode->i_fop));
	if (IS_ERR(f))
		return PTR_ERR(f);

	if (f->f_op->open != NULL)
		ret = f->f_op->open(inode, f);

	if (ret < 0) {
		put_filp(f);
		return ret;
	}

	f->f_path.dentry = NULL;
	*file = f;
	return 0;
}

static int __liblinux_kernfs_rw(unsigned long long pos, unsigned long ubuf,
				unsigned long size, void *ctx,
				unsigned long *rw_size /* out */, bool do_write)
{
	struct dentry dentry;
	struct kernfs_node *kn = NULL;
	struct file *f = NULL;
	int ret = -EINVAL;
	loff_t pos_loff;

	mutex_lock(&g_liblinux_lock);
	kn = idr_find(&sysfs_idr, *(int *)ctx);
	if ((kn == NULL) || (size > SIZE_MAX) || (pos > (unsigned long long)LLONG_MAX)) {
		mutex_unlock(&g_liblinux_lock);
		return -EINVAL;
	}
	kernfs_get(kn);
	mutex_unlock(&g_liblinux_lock);

	pos_loff = (loff_t)pos;
	memset(&dentry, 0, sizeof(struct dentry));
	dentry.d_fsdata = (void *)kn;
	dentry.d_inode = __liblinux_kernfs_iget(kn);
	if (dentry.d_inode == NULL)
		goto err_iget;

	/* not support !KERNFS_FILE open */
	BUG_ON(!(kernfs_type(kn) == KERNFS_FILE));
	ret = __liblinux_kernfs_open(&dentry, (do_write ? O_WRONLY : O_RDONLY), &f);
	if (ret < 0)
		goto err_open;

	if (do_write)
		ret = vfs_write_full(f, (const char __user *)(uintptr_t)ubuf,
					(size_t)size, &pos_loff);
	else
		ret = vfs_read_full(f, (char __user *)(uintptr_t)ubuf, (size_t)size, &pos_loff);

	if (ret >= 0) {
		*rw_size = ret;
		ret = 0;
	}

	fput(f);
err_open:
	iput(dentry.d_inode);
err_iget:
	kernfs_put(kn);
	return ret;
}

static int __liblinux_kernfs_read(unsigned long long pos, unsigned long ubuf,
				  unsigned long size, void *ctx,
				  unsigned long *rsize /* out */)
{
	return __liblinux_kernfs_rw(pos, ubuf, size, ctx, rsize, false);
}

static int __liblinux_kernfs_write(unsigned long long pos, unsigned long ubuf,
				   unsigned long size, void *ctx,
				   unsigned long *wsize /* out */)
{
	return __liblinux_kernfs_rw(pos, ubuf, size, ctx, wsize, true);
}

static int __liblinux_kernfs_poll(int (*callback)(void *_ctx, int pollable), int id, int pollable)
{
	struct kernfs_node *kn = NULL;
	int ret;

	mutex_lock(&g_liblinux_lock);
	kn = (struct kernfs_node *)idr_find(&sysfs_idr, id);
	if (kn == NULL) {
		mutex_unlock(&g_liblinux_lock);
		return -EINVAL;
	}
	kernfs_get(kn);
	mutex_unlock(&g_liblinux_lock);

	ret = callback((void *)kn, pollable);
	kernfs_put(kn);
	return ret;
}


static struct liblinux_pal_kernfs_ops liblinux_kernfs_fops = {
	.read = __liblinux_kernfs_read,
	.write = __liblinux_kernfs_write,
	.poll = __liblinux_kernfs_poll,
};

static int __liblinux_create_symlink(const char *path,
				     struct liblinux_kernfs_ops *ops,
				     struct kernfs_node *kn)
{
	int ret;
	char *targetpath = NULL;
	struct kernfs_node *target = kn->symlink.target_kn;

	targetpath = kzalloc(PATH_MAX, GFP_KERNEL);
	if (targetpath == NULL)
		return -ENOMEM;

	ret = kernfs_get_target_path(kn->parent, target, targetpath);
	if (ret < 0)
		goto err;

	ret = ops->create_symlink(path, kn->mode, targetpath);
err:
	kfree(targetpath);
	return ret;
}

static struct liblinux_kernfs_ops *
__liblinux_kernfs_acquire_ops(struct kernfs_node *kn)
{
	struct kernfs_root *root = kernfs_root(kn);

	if ((root == NULL) || (root->syscall_ops == NULL))
		return NULL;

	return container_of(root->syscall_ops, struct liblinux_kernfs_ops, kops);
}

static int __liblinux_sysfs_create_file(const char *path, struct kernfs_node *kn,
					struct liblinux_kernfs_ops *ops)
{
	int ret, id;

	mutex_lock(&g_liblinux_lock);
	id = idr_alloc_cyclic(&sysfs_idr, kn, liblinux_fs_tag_min, liblinux_fs_tag_max, GFP_NOWAIT);
	if (id < 0) {
		pr_err("liblinux sysfs idr alloc failed: %d\n", id);
		mutex_unlock(&g_liblinux_lock);
		return id;
	}

	ret = ops->create_file(path, kn->mode, NULL, (void *)(uintptr_t)id);
	if (ret < 0)
		idr_remove(&sysfs_idr, id);

	mutex_unlock(&g_liblinux_lock);

	return ret;
}

int liblinux_kernfs_create(struct kernfs_node *kn)
{
	int ret;
	char *buf = NULL;
	char *path = NULL;
	struct liblinux_kernfs_ops *ops = NULL;

	ops = __liblinux_kernfs_acquire_ops(kn);
	if (ops == NULL)
		return -EINVAL;

	buf = kzalloc(PATH_MAX, GFP_KERNEL);
	if (buf == NULL)
		return -ENOMEM;

	ret = kernfs_path(kn, buf, PATH_MAX);
	if (ret < 0)
		goto err;

	/* ops cann't handle path begin with '/' */
	path = buf + 1;

	ret = -EINVAL;
	switch (kernfs_type(kn)) {
	case KERNFS_DIR:
		if (ops->create_dir != NULL)
			ret = ops->create_dir(path, kn->mode);

		break;
	case KERNFS_FILE:
		if (ops->create_file != NULL)
			ret = __liblinux_sysfs_create_file(path, kn, ops);

		break;
	case KERNFS_LINK:
		if (ops->create_symlink != NULL)
			ret = __liblinux_create_symlink(path, ops, kn);

		break;
	default:
		break;
	}

err:
	kfree(buf);
	return ret;
}

static void hook_kernfs_add_one(void *__data, struct kernfs_node *kn,
				void (*remove)(struct kernfs_node *), int *ret)
{
	*ret = liblinux_kernfs_create(kn);
	if (*ret < 0) {
		/*
		 * kernfs_get() do count + 1. then it will not clean up
		 * kernfs_node in __kernfs_remove(), when kernfs create failed.
		 */
		kernfs_get(kn);
		remove(kn);
	}
}
INIT_VENDOR_HOOK(ldk_vh_kernfs_add_one, hook_kernfs_add_one);

static int _liblinux_compare(int id, void *data, void *kn)
{
	if (data == kn)
		return id;

	return 0;
}

int liblinux_kernfs_unlink(struct kernfs_node *kn)
{
	int ret, id;
	char *buf = NULL;
	char *path = NULL;
	struct liblinux_kernfs_ops *ops = NULL;

	mutex_lock(&g_liblinux_lock);
	if (!kn || !atomic_dec_and_test(&kn->count)) {
		mutex_unlock(&g_liblinux_lock);
		return -EINVAL;
	}
	id = idr_for_each(&sysfs_idr, _liblinux_compare, kn);
	if (id >= 0)
		idr_remove(&sysfs_idr, id);

	mutex_unlock(&g_liblinux_lock);

	ops = __liblinux_kernfs_acquire_ops(kn);
	if (ops == NULL)
		goto err;

	buf = kzalloc(PATH_MAX, GFP_KERNEL);
	if (buf == NULL)
		goto err;

	ret = kernfs_path(kn, buf, PATH_MAX);
	if (ret < 0)
		goto path_err;

	/* ops cann't handle path begin with '/' */
	path = buf + 1;

	if (ops->unlink != NULL)
		ret = ops->unlink(path);

path_err:
	kfree(buf);
err:
	/* ignore hm sysfs unlink failed, continue to run kernfs_put */
	return 0;
}

static void hook_kernfs_put(void *__data, struct kernfs_node *kn, int *ret)
{
	*ret = liblinux_kernfs_unlink(kn);
}
INIT_VENDOR_HOOK(ldk_vh_kernfs_put, hook_kernfs_put);

ssize_t vfs_read_full(struct file *filep, char __user *buf, size_t size, loff_t *pos)
{
	ssize_t ret = 0;
	ssize_t read_bytes = 0;

	do {
		ret = vfs_read(filep, buf + read_bytes, size - read_bytes, pos);
		if (ret == 0)
			break;
		else if (ret < 0)
			return ret;
		else
			read_bytes += ret;
	} while (read_bytes < size);

	if (read_bytes == size) {
		/* try read more from `pos` */
		ret = vfs_read(filep, buf, PAGE_SIZE, pos);
		if (ret > 0)
			ret = -EFBIG;
	}

	return (ret < 0) ? ret : read_bytes;
}

ssize_t vfs_write_full(struct file *filep, const char __user *buf, size_t size, loff_t *pos)
{
	ssize_t ret = 0;
	ssize_t write_bytes = 0;

	do {
		ret = vfs_write(filep, buf + write_bytes, size - write_bytes, pos);
		if (ret == 0)
			break;
		else if (ret == -ERESTARTNOINTR)
			continue;
		else if (ret < 0)
			return ret;
		else
			write_bytes += ret;

	} while (write_bytes < size);

	if (write_bytes != size)
		ret = -EINVAL;

	return (ret < 0) ? ret : write_bytes;
}

int liblinux_kernfs_notify(struct kernfs_node *kn)
{
	int ret;
	char *buf = NULL;
	char *path = NULL;
	struct liblinux_kernfs_ops *ops = NULL;

	ops = __liblinux_kernfs_acquire_ops(kn);
	if (ops == NULL)
		return -EINVAL;

	buf = kzalloc(PATH_MAX, GFP_KERNEL);
	if (buf == NULL)
		return -ENOMEM;

	ret = kernfs_path(kn, buf, PATH_MAX);
	if (ret < 0)
		goto err;

	/* ops cann't handle path begin with '/' */
	path = buf + 1;
	if (ops->notify != NULL)
		ret = ops->notify(kn, path);
	else
		ret = 0;
err:
	kfree(buf);
	return ret;
}

static void hook_kernfs_notify(void *__data, struct kernfs_node *kn)
{
	liblinux_kernfs_notify(kn);
}
INIT_VENDOR_HOOK(ldk_vh_kernfs_notify, hook_kernfs_notify);

static int __init ldk_auto_mount_sys(void)
{
	int ret;
	const char *mp_dir = "/sys";

	ret = init_mkdir(mp_dir, 0755);
	if (ret && ret != -EEXIST) {
		pr_err("mkdir `%s` failed, err=%d\n", mp_dir, ret);
		return ret;
	}
	ret = init_mount("sysfs", mp_dir, "sysfs", MS_SILENT, NULL);
	if (ret) {
		pr_err("auto mount sysfs failed, err=%d\n", ret);
		return ret;
	}
	pr_debug("auto mount sysfs success\n");
	return 0;
}
late_initcall(ldk_auto_mount_sys)

int liblinux_sysfs_init(void)
{
	int ret;
	struct file_system_type *sys_fs_type;
	void *ptr_ret;

	if (!sysfs_mnt) {
		sys_fs_type = get_fs_type("sysfs");
		if (!sys_fs_type) {
			pr_warn("Failed to find sysfs\n");
			return -EINVAL;
		}
		ptr_ret = kern_mount(sys_fs_type);
		put_filesystem(sys_fs_type);
		if (IS_ERR(ptr_ret)) {
			pr_warn("Failed to mount sysfs\n");
			return PTR_ERR(ptr_ret);
		}
		sysfs_mnt = ptr_ret;
	}
	mutex_init(&g_liblinux_lock);

	ret = liblinux_pal_require_frange(LIBLINUX_SYSFS, &liblinux_fs_tag_min,
						&liblinux_fs_tag_max);
	if (ret < 0) {
		pr_warn("require frange failed ret=%d\n", ret);
		return ret;
	}
	return liblinux_pal_sysfs_register_ops(&liblinux_kernfs_fops);
}

static void hook_sysfs_init(void *__data, struct kernfs_root **root,
					struct file_system_type *sys_fs_op, int *ret)
{
	static struct liblinux_kernfs_ops liblinux_sysfs_ops = {
		.create_dir = liblinux_sysfs_create_dir,
		.create_file = liblinux_sysfs_create_file,
		.create_symlink = liblinux_sysfs_create_link,
		.unlink = liblinux_sysfs_delete,
		.notify = liblinux_sysfs_notify,
	};

	memset(&liblinux_sysfs_ops.kops, 0, sizeof(struct kernfs_syscall_ops));
	*root = kernfs_create_root(&liblinux_sysfs_ops.kops,
					KERNFS_ROOT_EXTRA_OPEN_PERM_CHECK, NULL);
	if (IS_ERR(*root)) {
		*ret = PTR_ERR(*root);
		return;
	}

	sysfs_root_kn = (*root)->kn;

	*ret = register_filesystem(sys_fs_op);
	if (*ret) {
		kernfs_destroy_root(*root);
		return;
	}

	*ret = liblinux_sysfs_init();
	if (*ret < 0)
		panic("mount pseudo_fs failed, ret %d\n", *ret);
	*ret = 0;
}
INIT_VENDOR_HOOK(ldk_vh_sysfs_init, hook_sysfs_init);
